package com.avp;

import java.util.ArrayList;
import java.util.TreeSet;
import java.net.*;
import java.io.*;
import java.util.Timer;
import java.util.TimerTask;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.LinkedList;

public class DirectoryServer extends Thread{
	public static final int defaultPort = 9999;
	public static final int maxPing = 3;
	private int _port;
	private TreeSet<User> _directory;
	private HashMap<Socket, User> socketMap;
	public DirectoryServer(int port) {
		_directory = new TreeSet<User>();
		socketMap = new HashMap<Socket, User>();
		_port = port;
	}
	public DirectoryServer() {
		this(DirectoryServer.defaultPort);
	}
	private boolean addUser(User u) {
		//TreeSets are not thread safe
		LinkedList<Socket> remSockets = new LinkedList<Socket>();
		if(u.getUserName()!=null) {
			boolean ret;
			synchronized(_directory) {
				ret = _directory.add(u);
				printUsers();
			}
			if(u.getUserName()!=null) {
				Set<Socket> keyset = socketMap.keySet();
				for(Socket s : keyset) {
					if(socketMap.get(s).getUserName()!=null && !socketMap.get(s).getUserName().equals(u.getUserName()) && s.isConnected()) {
						try {
							s.getOutputStream().write(String.format("3:%s\r\n", u.getUserName()).getBytes());
						}
						catch(IOException ioEx) {
							remSockets.add(s);
						}
					}
				}
			}
			for(Socket sock : remSockets) {
				removeSocket(sock);
			}
			return ret;
		}
		return false;
	}
	private boolean removeUser(User u) {
		//TreeSets aren't thread safe
		boolean ret;
		synchronized(_directory) {
			ret = _directory.remove(u);
			printUsers();
		}
		//Below here isn't a big deal for threading hopefully
		LinkedList<Socket> remSockets = new LinkedList<Socket>();
		if(u.getUserName()!=null) {
			System.out.printf("User %s has left the server\n", u.getUserName());
			Set<Socket> keyset = socketMap.keySet();
			for(Socket s : keyset) {
				if(socketMap.get(s).getUserName()!=null && !socketMap.get(s).getUserName().equals(u.getUserName())) {
					try {
						s.getOutputStream().write(String.format("4:%s\r\n", u.getUserName()).getBytes());
					}
					catch(IOException ioEx) {
						remSockets.add(s);
					}
				}
			}
		}
		for(Socket sock : remSockets) {
			removeSocket(sock);
		}
		return ret;
	}
	//This function is only used in add/remove user inside the synchronize block
	private void printUsers() {
		System.out.println("User list:");
		for(User u : _directory) {
			System.out.println(u.getUserName());
		}
		System.out.println("End list.");
	}
	private ArrayList<String> listUsers() {
		ArrayList<String> userList = new ArrayList<String>();
		synchronized(_directory) {
			System.out.println("User list:");
			for(User u : _directory) {
				System.out.println("user: "+u.getUserName());
				userList.add(u.getUserName());
			}
		}
		return userList;
	}
	private boolean isLoggedIn(User u) {
		//Still not thread safe
		synchronized(_directory) {
			return _directory.contains(u);
		}
	}
	private boolean addSocket(Socket s, User u) {
		//Assumed to not be thread safe
		synchronized(socketMap) {
			return socketMap.put(s, u)==null;
		}
	}
	private boolean removeSocket(Socket s) {
		System.out.println("removing socket");
		//HashMap assumed to not be thread safe
		boolean ret;
		synchronized(socketMap) {
			User u = socketMap.remove(s);
			if(u!=null) {
				removeUser(u);
				ret = true;
			}
			ret = false;
		}
		if(!s.isClosed()) {
			try{
				s.close();
			}
			catch(Exception e) {
				//If there's an exception here it's not a big deal since the socket is probably
				//closed
			}
		}
		return ret;
	}
	public static void main(String[] args) {
		DirectoryServer server = new DirectoryServer();
		server.run();
	}
	public void run() {
		Timer t = new Timer();
		AcceptThread at = new AcceptThread(_directory, _port);
		//t.scheduleAtFixedRate(new PingThread(), 5000, 10000);
		at.run();
		try {
			at.join();
			//This should pretty much never end
		}
		catch(Exception e) {
			//Not important
		}
	}
	//This thread handles new connections and will pass it to another thread which will take care
	//of functionality.
	private class AcceptThread extends Thread {
		private TreeSet<User> _myDirectory;
		private ServerSocket _masterSocket;
		public AcceptThread(TreeSet<User> directory, int port) {
			_myDirectory = directory;
			try {
				_masterSocket = new ServerSocket(port, 10);
			}
			catch(IOException ioEx) {
				System.err.println("Error: Could not open directory server master socket");
				System.exit(1);
			}
		}
		public void run() {
			while(true) {
				try {
					Socket clientSocket = _masterSocket.accept();
					Thread t = new FunctionThread(clientSocket, _myDirectory);
					t.start();
				}
				catch(IOException ioEx) {
					System.err.println("Error accepting socket connection");
					continue;
				}
			}
		}
	}
	private class FunctionThread extends Thread {
		private TreeSet<User> _myDirectory;
		private Socket _slaveSocket;
		private BufferedReader _input;
		private PrintWriter _output;
		public FunctionThread(Socket slaveSocket, TreeSet<User> myDirectory) {
			_myDirectory = myDirectory;
			_slaveSocket = slaveSocket;
			try {
				_input = new BufferedReader(new InputStreamReader(slaveSocket.getInputStream()));
				_output = new PrintWriter(slaveSocket.getOutputStream());
			}
			catch(IOException ioEx) {
				System.err.println("Error initializing functional IO sockets");
				_input = null;
			}
		}
		public void run() {
			String inputString;
			String inputLine[];
			SocketAddress remoteAddress = _slaveSocket.getRemoteSocketAddress();
			User client = new User(null, remoteAddress);
			System.out.println("New client connection");
			socketMap.put(_slaveSocket, client);
			while(true) {
					try {
						if(_slaveSocket.isClosed()) break;
						if(_input.ready()) {
							inputString = _input.readLine();
							if(inputString==null) break;
							inputLine = inputString.split("\\s+");
							if(inputLine[0].equals("login")) {
								if(client.getUserName()==null && inputLine.length==2) {
									//Only allow login if the user isn't already logged in
									client.setUserName(inputLine[1]);
									addUser(client);
									System.out.printf("User %s has logged in\n", inputLine[1]);
								}
							}
							else if(inputLine[0].equals("logout")) {
								System.out.println("User requested logout");
								if(removeUser(client)) {
//									System.out.printf("User %s has left the server\n", client.getUserName());
									client.setUserName(null);
								}
							}
							else if(inputLine[0].equals("list")) {
								//Change this later to format a string and write it all at the same time
								System.out.println("Get user list");
								if(isLoggedIn(client)) {
									ArrayList<String> userList = listUsers();
									_output.print("1:");
									for(String s : userList) {
										if(!(s.equals(client.getUserName())))
											_output.printf("%s ", s);
									}
									_output.printf("\r\n");
									_output.flush();
								}
							}
							else if(inputLine[0].equals("pong")) {
								client.resetPingTicks();
							}
							else if(inputLine[0].equals("getip")) {
								synchronized(_directory) {
									_output.print("2:");
									int count = 0;
									for(User u : _directory) {
										String s = inputLine[1];
										if(u.getUserName().equals(s) && count==0) {
											_output.print(((InetSocketAddress)u.getIP()).getAddress().getHostAddress());//+":"+((InetSocketAddress)u.getIP()).getPort());
											count++;
										}
									}
									_output.print("\r\n");
									_output.flush();
								}
							}
							else if(inputLine[0].equals("quit")) {
								removeSocket(_slaveSocket);
							}
						}
					}
					catch(IOException ioEx) {
						System.err.println("Error: Buffered reader broken");
						removeSocket(_slaveSocket);
						return;
					}
			}
			if(client.getUserName()!=null && client.getUserName().length()>0) {
			//	System.out.printf("User %s has left the server\n", client.getUserName());
				System.out.flush();
			}
			removeSocket(_slaveSocket);
		}
	}
	//This is to monitor a connection so if the lag is too bad then the
	//client is disconnected.  Ping intervals and # of pings to timeout
	//is customizable.  Currently not linked in at all
	private class PingThread extends TimerTask {

		public void run() {
			Socket[] sockets;
			synchronized(socketMap) {
				for(Socket s : socketMap.keySet()) {
					try {
						s.getOutputStream().write(String.format("ping\r\n").getBytes());
					}
					catch(IOException ioEx) {
						System.err.println("Error: Ping failed");
					}
					User u = socketMap.get(s);
					u.incrementPing();
					if(u.getPing()>DirectoryServer.maxPing) {
						socketMap.remove(s);
						removeSocket(s);
					}
				}
			}
		}
	}
}
